iOS 使用NSMethodSignature和 NSInvocation进行 method 或 block的调用

From: https://juejin.im/post/5a30c7c151882503eb4b44e2

这篇博文是我的另一篇 Aspects源码剖析中的一部分,考虑到这部分内容相对独立,单独成篇以便查询。

使用NSMethodSignatureNSInvocation 不仅可以完成对method的调用,也可以完成block的调用。在Aspect中,正是运用NSMethodSignature,NSInvocation 实现了对block的统一处理。这篇博文将演示NSMethodSignatureNSInvocation的使用方法及如何使用他们执行method 或 block。

##对象调用method代码示例 一个实例对象可以通过三种方式调用其方法。

- (void)test{

//type1
    [self printStr1:@"hello world 1"];

//type2
    [self performSelector:@selector(printStr1:) withObject:@"hello world 2"];

//type3
    //获取方法签名
    NSMethodSignature *sigOfPrintStr = [self methodSignatureForSelector:@selector(printStr1:)];

    //获取方法签名对应的invocation
    NSInvocation *invocationOfPrintStr = [NSInvocation invocationWithMethodSignature:sigOfPrintStr];

    /**
    设置消息接受者,与[invocationOfPrintStr setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等价
    */
    [invocationOfPrintStr setTarget:self];

    /**设置要执行的selector。与[invocationOfPrintStr setArgument:@selector(printStr1:) atIndex:1] 等价*/
    [invocationOfPrintStr setSelector:@selector(printStr1:)];

    //设置参数 
    NSString *str = @"hello world 3";
    [invocationOfPrintStr setArgument:&str atIndex:2];

    //开始执行
    [invocationOfPrintStr invoke];
}

- (void)printStr1:(NSString*)str{
    NSLog(@"printStr1  %@",str);
}
复制代码

在调用test方法时,会分别输出:

2017-01-11 15:20:21.642 AspectTest[2997:146594] printStr1  hello world 1
2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1  hello world 2
2017-01-11 15:20:21.643 AspectTest[2997:146594] printStr1  hello world 3
复制代码

type1和type2是我们常用的,这里不在赘述,我们来说说type3。 NSMethodSignatureNSInvocationFoundation框架为我们提供的一种调用方法的方式,经常用于消息转发。

##NSMethodSignature概述

NSMethodSignature用于描述method的类型信息:返回值类型,及每个参数的类型。 可以通过下面的方式进行创建:

@interface NSObject 
//获取实例方法的签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
//获取类方法的签名
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;
@end

-------------
//使用ObjCTypes创建方法签名
@interface NSMethodSignature
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
@end
复制代码

使用NSObject的实例方法和类方法创建NSMethodSignature很简单,不说了。咱撩一撩signatureWithObjCTypes。 在OC中,每一种数据类型可以通过一个字符编码来表示(Objective-C type encodings)。例如字符‘@’代表一个object, ‘i’代表int。 那么,由这些字符组成的字符数组就可以表示方法类型了。举个例子:上面提到的printStr1:对应的ObjCTypes 为 v@:@。

  • ’v‘ : void类型,第一个字符代表返回值类型
  • ’@‘ : 一个id类型的对象,第一个参数类型
  • ’:‘ : 对应SEL,第二个参数类型
  • ’@‘ : 一个id类型的对象,第三个参数类型,也就是- (void)printStr1:(NSString*)str中的str。

printStr1:本来是一个参数,ObjCTypes怎么成了三个参数?要理解这个还必须理解OC中的消息机制。一个method对应的结构体如下,ObjCTypes中的参数其实与IMP method_imp 函数指针指向的函数的参数相一致。相关内容有很多,不了解的可以参考这篇文章[方法与消息][3]。

[3]: https://link.juejin.im?target=http%3A%2F%2Fwww.cocoachina.com%2Fios%2F20141106%2F10150.html

typedef struct objc_method Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char
method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
复制代码

##NSInvocation概述

就像示例代码所示,我们可以通过+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;创建出NSInvocation对象。接下来你设置各个参数信息, 然后调用invoke进行调用。执行结束后,通过- (void)getReturnValue:(void *)retLoc;获取返回值。 这里需要注意,对NSInvocation对象设置的参数个数及类型和获取的返回值的类型要与创建对象时使用的NSMethodSignature对象代表的参数及返回值类型向一致,否则cresh。

##使用NSInvocation调用block 下面展示block 的两种调用方式

- (void)test{

    void (^block1)(int) = ^(int a){
         NSLog(@"block1 %d",a);
    };

    //type1
    block1(1);

    //type2
    //获取block类型对应的方法签名。
    NSMethodSignature *signature = aspect_blockMethodSignature(block1);
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setTarget:block1];
    int a=2;
    [invocation setArgument:&a atIndex:1];
    [invocation invoke];
}
复制代码

type1 就是常用的方法,不再赘述。看一下type2。 type2和上面调用method的type3用的一样的套路,只是参数不同:由block生成的NSInvocation对象的第一个参数是block本身,剩下的为 block自身的参数。

由于系统没有提供获取block的ObjCTypes的api,我们必须想办法找到这个ObjCTypes,只有这样才能生成NSMethodSignature对象! ###block的数据结构 & 从数据结构中获取 ObjCTypes oc是一门动态语言,通过编译 oc可以转变为c语言。经过编译后block对应的数据结构是struct。(block中技术点还是挺过的,推荐一本书“Objective-C 高级编程”)

//代码来自 Aspect
// Block internals.
typedef NS_OPTIONS(int, AspectBlockFlags) {
        AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
        AspectBlockFlagsHasSignature          = (1 << 30)
};
typedef struct _AspectBlock {
        __unused Class isa;
        AspectBlockFlags flags;
        __unused int reserved;
        void (__unused *invoke)(struct _AspectBlock *block, ...);
        struct {
                unsigned long int reserved;
                unsigned long int size;
                // requires AspectBlockFlagsHasCopyDisposeHelpers
                void (*copy)(void *dst, const void *src);
                void (*dispose)(const void *);
                // requires AspectBlockFlagsHasSignature
                const char *signature;
                const char *layout;
        } *descriptor;
        // imported variables
} *AspectBlockRef;
复制代码

在此结构体中 const char *signature 字段就是我们想要的。通过下面的方法获取signature并创建NSMethodSignature对象。

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
    AspectBlockRef layout = (__bridge void *)block;
        if (!(layout->flags & AspectBlockFlagsHasSignature)) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
        void *desc = layout->descriptor;
        desc += 2 * sizeof(unsigned long int);
        if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
                desc += 2 * sizeof(void *);
    }
        if (!desc) {
        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
        AspectError(AspectErrorMissingBlockSignature, description);
        return nil;
    }
        const char *signature = (*(const char **)desc);
        return [NSMethodSignature signatureWithObjCTypes:signature];
}

另外一篇

iOS 使用NSMethodSignature和 NSInvocation进行 method 或 block的调用

http://chenzhao.date/2018/10/23/iOS 使用NSMethodSignature和 NSInvocation进行 method 或 block的调用.html

Author

陈昭

Posted on

2018-10-23

Updated on

2021-12-27

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

Kommentare

You forgot to set the shortname for Disqus. Please set it in _config.yml.